htmlレンダリング: propsの実装
htmlタグを描画できるようにはなったが、それだけだと不十分。
propsの実装をして、クリックイベントや style を使えるようにする。
直接 renderVnode に実装をしてもいいが、本家に倣った実装にする
modulesディレクトリ
class や style, その他 props の操作をするためのファイルを実装
patchProps
それらをまとめる関数
patchEvent: イベントを紐づける関数
patchAttr: classやスタイルなど(イベント以外)を紐づける関数
patchProp のガワを作成
code: runtime-dom/patchProp.ts
type DOMRendererOptions = RendererOptions<Node, Element>
export const isOn = (key: string) => onRE.test(key)
export const patchProp: DOMRendererOptions'patchProp' = (el, key, value) => { // イベントの場合とそうでない場合で分岐
if (isOn(key)) {
// patchEvent(el, key, value); // これから実装
} else {
// patchAttr(el, key, value); // これから実装
}
}
RendererOptions に patchProp の型がないので追加
code: packages/runtime-core/render.ts
export interface RendererOptions<
HostNode = RendererNode,
HostElement = RendererElement
{
patchProp(el: HostElement, key: string, value: any): void;
// 省略
}
それに伴って、nodeOps では patchProp 以外の部分を使用するように書き換え
NOTE: nodeOpsに書いてもよくね?? わざわざomitする理由は?
-> patchProp の処理が複雑になるから分けた
-> dirの整理上の問題
code: packages/runtime-dom/nodeOps.ts
export const nodeOps: Omit<RendererOptions, "patchProp"> = {
// 省略
}
rendererを生成する際に、patchProp も一緒に渡すように変更
code: packages/runtime-dom/index.ts
import { patchProp } from './patchProp';
const { render } = createRenderer({ ...nodeOps, patchProp })
イベントハンドラ
patchEventを実装する
長いが特に難しいことはしてない
エレメントに重複してイベントを登録しないために _veiをはやしているとことがちょっとテクいぐらい
code: packages/runtime-dom/modules/events.ts
// 登録するイベントを invoker という関数でラップしておく
interface Invoker extends EventListener {
value: EventValue
}
type EventValue = Function
// el に event 名のイベント (handler) を登録する関数
export function addEventListener(
el: Element,
event: string,
handler: EventListener,
) {
el.addEventListener(event, handler)
}
// イベントをremoveする関数
export function removeEventListener(
el: Element,
event: string,
handler: EventListener,
) {
el.removeEventListener(event, handler)
}
/**
*
* @param el エレメント
* @param rawName イベント名
* @param value イベント関数
*/
export function patchEvent(
el: Element & { _vei?: Record<string, Invoker | undefined> },
rawName: string,
value: EventValue | null,
) {
// 同じ要素に対して重複してイベントを登録するのを防ぐために、要素に_vei(vue event invokers) という名前で invokersを生やしてあげる
// patch時にこれを更新すればいいだけになり、重複しての登録を防げる!
const invokers = el._vei || (el._vei = {})
const existingInvoker = invokersrawName if (value && existingInvoker) {
// 既にイベント登録済みのエレメントに新しいイベント(value)が紐付けられる
existingInvoker.value = value
} else {
// onClick, onInput を click, inputなどのイベント名に変換
const name = parseName(rawName)
if (value) {
// valueがある場合はイベントを新しく紐付け
// TODO: ここで invokersrawName を更新している意味? 別に再度この処理が走るわけではないから必要なくない? const invoker = (invokersrawName = createInvoker(value)) addEventListener(el, name, invoker)
} else if (existingInvoker) {
// 既にイベント登録済みのエレメントにイベント(value)がない場合は、イベントをremoveする
removeEventListener(el, name, existingInvoker)
}
}
}
function parseName(rawName: string): string {
return rawName.slice(2).toLocaleLowerCase()
}
function createInvoker(initialValue: EventValue) {
const invoker: Invoker = (e: Event) => {
invoker.value(e)
}
invoker.value = initialValue
return invoker
}
あとは patchProps に組み込んで、renderVnode で使うようにする
code: packages/runtime-dom/patchProp.ts
export const patchProp: DOMRendererOptions'patchProp' = (el, key, value) => { if (isOn(key)) {
patchEvent(el, key, value) // <- 追加!
} else {
// patchAttr(el, key, value); // これから実装する
}
}
code: packages/runtime-core/renderer.ts
const {
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
insert: hostInsert,
} = options;
// 省略
function renderVNode(vnode: VNode | string) {
if (typeof vnode === "string") return hostCreateText(vnode);
const el = hostCreateElement(vnode.type);
// 追加!
Object.entries(vnode.props).forEach((key, value) => { hostPatchProp(el, key, value);
});
// 省略
これでイベントの登録ができるようになった!!
例: 簡単なアラートの表示をする
code: examples/playground/main.ts
import { createApp, h } from 'chibivue'
const app = createApp({
render() {
return h('div', {}, [
h(
'button',
{
onClick() {
alert('Hello world!')
},
},
),
])
},
})
app.mount('#app')
他のPropsにも対応してみる
setAttribute を実装する (超簡単!!)
code: packages/runtime-dom/modules/attrs.ts
export function patchAttr(el: Element, key: string, value: any) {
if (value === null) {
el.removeAttribute(key);
} else {
el.setAttribute(key, value);
}
};
で完成!!